﻿namespace Bowlus.AppHost
{
    using System;
    using System.Collections.Concurrent;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Globalization;
    using System.IO;
    using System.Linq;
    using System.Security.AccessControl;
    using System.Threading;
    using System.Threading.Tasks;

    using Bowlus.Common;
    using Bowlus.Common.Communications;

    using Microsoft.Web.Administration;
    using Microsoft.WindowsAzure.ServiceRuntime;

    public class AppHostManager
    {
        private readonly List<IISAppInfo> installedApplications = new List<IISAppInfo>();

        private readonly IAppRepository appRepository;

        // TODO: do we need these? queue, task ...
        // YES WE DO, MULTIPLE MESSAGES COMING IN DIFFERENT THREADS, NEED TO SERIALIZE THE INCOMING REQUESTS
        private readonly ConcurrentQueue<BowlusMessage> requestQueue;

        private readonly Task deploymentTask;

        private string sitesPath;

        private string appCachePath;

        private static readonly object LockObject = new object();

        private bool initialized;

        public AppHostManager(IAppRepository repository)
        {
            this.appRepository = repository;
            this.requestQueue = new ConcurrentQueue<BowlusMessage>();
            this.deploymentTask = new Task(this.DeploymentRequestHandler);
            this.initialized = false;
        }

        /// <summary>
        /// Initializes the deployment manager
        /// </summary>
        public void Initialize()
        {
            try
            {
                // Initialize Local Resources
                this.sitesPath = RoleEnvironment.GetLocalResource("Sites").RootPath.TrimEnd('\\');
                this.appCachePath = RoleEnvironment.GetLocalResource("AppCache").RootPath.TrimEnd('\\');

                // Configure Security on our directories
                this.ConfigureDirectorySecurity(this.sitesPath);
                this.ConfigureDirectorySecurity(this.appCachePath);

                this.deploymentTask.Start();
            }
            catch (Exception ex)
            {
                Trace.TraceError(ex.Message);
                throw;
            }
        }

        public void MessageHandler(BowlusMessage message)
        {
            Trace.TraceInformation(string.Format("*** Message {0} received from {1} with details: {2}", Enum.GetName(typeof(BowlusMessageType), message.MessageType), message.Sender, message.Body));
            this.requestQueue.Enqueue(message);
        }

        private static ApplicationPool GetAppPool(ServerManager sm, string siteName)
        {
            var appPool = sm.ApplicationPools.SingleOrDefault(p => p.Name == siteName);
            if (appPool == null)
            {
                appPool = sm.ApplicationPools.Add(siteName);
                appPool.ManagedRuntimeVersion = "v4.0";
                appPool.ProcessModel.LoadUserProfile = false;
            }

            return appPool;
        }

        private static bool RemoveIISApp(IISAppInfo app)
        {
            //// Delete sites we dont have any more
            //foreach (var site in sm.Sites.Where(s => s.Name.StartsWith(RoleEnvironment.CurrentRoleInstance.Id)).ToArray())
            //{
            //    var name = site.Name.Substring(RoleEnvironment.CurrentRoleInstance.Id.Length + 1).ToLower();
            //    var entry = this.installedApplications.SingleOrDefault(e => e.HostName == name);

            //    // never delete "Web," which is the website for this web role
            //    if (entry == null && name != "web")
            //    {
            //        var appPoolName = site.Applications[0].ApplicationPoolName;
            //        var appPool = sm.ApplicationPools.SingleOrDefault(ap => ap.Name == appPoolName);
            //        sm.ApplicationPools.Remove(appPool);
            //        sm.Sites.Remove(site);
            //    }
            //}

            //// Delete folders we don't use any more
            //foreach (var directory in Directory.EnumerateDirectories(this.sitesPath))
            //{
            //    var host = directory.Replace(this.sitesPath, string.Empty).Replace("\\", string.Empty);
            //    if (this.installedApplications.Count(e => e.HostName == host) == 0)
            //    {
            //        Directory.Delete(directory);
            //    }
            //}

            return false;
        }

        private static bool AddIISApp(IISAppInfo app)
        {
            lock (LockObject)
            {
                try
                {
                    var serverManager = new ServerManager();

                    var siteName = RoleEnvironment.CurrentRoleInstance.Id + "_" + app.ApplicationHostName;
                    var sitePath = Path.Combine(app.CurrentPath, "Site");

                    if (serverManager.Sites.Any(s => s.Name.Equals(siteName)))
                    {
                        Trace.TraceError(
                            "Site {0} ALREADY EXISTS on server {1}",
                            siteName,
                            RoleEnvironment.CurrentRoleInstance.Id);

                        return true;
                    }

                    // default binding is just the site name (example.org)
                    var newSite = serverManager.Sites.Add(
                        siteName,
                        "http",
                        RoleEnvironment.CurrentRoleInstance.InstanceEndpoints["Http"].IPEndpoint + ":" + app.ApplicationHostName,
                        sitePath);

                    var hostHeaderParts = app.ApplicationHostName.Split('.');
                    var appPoolName = hostHeaderParts.Length == 3 ? hostHeaderParts[0] : app.ApplicationHostName;

                    newSite.Applications.First().ApplicationPoolName = GetAppPool(serverManager, appPoolName).Name;

                    serverManager.CommitChanges();

                    return true;
                }
                catch (Exception exception)
                {
                    Trace.TraceError(
                        "Cannot create site for app {0} on server {1}. Details are {2}",
                        app.ApplicationHostName,
                        RoleEnvironment.CurrentRoleInstance.Id,
                        exception);

                    return false;
                }
            }
        }

        private void SendStatusMessage(BowlusMessageType messageType)
        {
            var commsClient = new CommunicationsClient();

            var appHost = new ApplicationHost()
            {
                InstanceId = RoleEnvironment.CurrentRoleInstance.Id,
                DeploymentManagerEndpoint = RoleEnvironment.CurrentRoleInstance.InstanceEndpoints[Settings.AppHostRoleName].IPEndpoint,
                Status = messageType.ToString()
            };

            var response = new BowlusMessage
            {
                Body = appHost.Serialize(),
                MessageType = messageType,
                MessageId = Guid.NewGuid(),
                Sender = RoleEnvironment.CurrentRoleInstance.InstanceEndpoints[Settings.AppHostRoleName].IPEndpoint
            };

            commsClient.SendMessageToRoleInstance(
                response,
                RoleEnvironment.Roles[Settings.FrontEndRoleName].Instances[0].InstanceEndpoints[Settings.FrontEndRoleName].IPEndpoint,
                Settings.FrontEndRoleName);
        }

        private void DeploymentRequestHandler()
        {
            while (true)
            {
                Thread.Sleep(TimeSpan.FromSeconds(5));

                BowlusMessage message;
                if (!this.requestQueue.TryDequeue(out message))
                {
                    continue;
                }

                Trace.TraceInformation(string.Format("AppHost: {0} Processing message {1}", RoleEnvironment.CurrentRoleInstance.Id, message.Body));

                switch (message.MessageType)
                {
                    case BowlusMessageType.Hearthbeat:
                        if (initialized)
                        {
                            SendStatusMessage(BowlusMessageType.AppHostStarted);
                        }
                        else
                        {
                            SendStatusMessage(BowlusMessageType.AppHostStartingUp);
                            initialized = true;
                        }
                        break;
                    case BowlusMessageType.DeployApp:
                        try
                        {
                            var tenantDeployment = new TenantDeployment(message.Body) { Status = TenantDeploymentStatus.DeployRequestReceived };

                            TenantDeploymentsStore.UpdateDeploymentStore(tenantDeployment);

                            lock (LockObject)
                            {
                                tenantDeployment = this.DeployApplication(tenantDeployment);
                            }

                            var routingManagerServiceClient = new CommunicationsClient();

                            var response = new BowlusMessage
                                {
                                    Body = tenantDeployment.Serialize(),
                                    MessageType = tenantDeployment.Status.Equals(TenantDeploymentStatus.AppDeployed) ? BowlusMessageType.AppDeployed : BowlusMessageType.AppDeployFailed,
                                    MessageId = message.MessageId,
                                    Sender = RoleEnvironment.CurrentRoleInstance.InstanceEndpoints[Settings.AppHostRoleName].IPEndpoint
                                };
                            routingManagerServiceClient.SendMessageToRoleInstance(response, message.Sender, Settings.FrontEndRoleName);
                        }
                        catch (Exception exception)
                        {
                            Trace.TraceError(string.Format("EXCEPTION during app deploy {0}", exception));
                        }


                        break;
                    default:
                        Trace.Write("Unsupported notify type: " + message);
                        break;
                }
            }
        }

        /// <summary>
        /// Configure the directory security.
        /// </summary>
        /// <param name="path">
        /// The path.
        /// </param>
        private void ConfigureDirectorySecurity(string path)
        {
            var sec = Directory.GetAccessControl(this.sitesPath);
            sec.AddAccessRule(new FileSystemAccessRule("Everyone", FileSystemRights.FullControl, InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit, PropagationFlags.None, AccessControlType.Allow));
            Directory.SetAccessControl(path, sec);
        }

        private TenantDeployment DeployApplication(TenantDeployment tenantDeployment)
        {
            Trace.TraceInformation(
                "Deploying application {0} for tenant {1} on server {2}",
                tenantDeployment.Application,
                tenantDeployment.TenantId,
                RoleEnvironment.CurrentRoleInstance.Id);

            if (this.installedApplications.Any(app => app.ApplicationHostName.Equals(tenantDeployment.HostName)))
            {
                Trace.TraceError(
                "Application {0} for tenant {1} ALREADY EXISTS on server {2}",
                tenantDeployment.Application,
                tenantDeployment.TenantId,
                RoleEnvironment.CurrentRoleInstance.Id);

                tenantDeployment.Status = TenantDeploymentStatus.AppDeployed;

                return tenantDeployment;
            }

            // 1. extract tenant.zip can copy to IIS app folder
            var appPath = Path.Combine(this.sitesPath, tenantDeployment.HostName, DateTime.UtcNow.Ticks.ToString(CultureInfo.InvariantCulture));
            this.appRepository.ExtractTenantApplication(tenantDeployment, appPath);

            // 2. add a webApp through IIS management API
            var newApp = new IISAppInfo
                {
                    ApplicationHostName = tenantDeployment.HostName,
                    CurrentPath = appPath,
                    Updated = (new FileInfo(appPath)).LastWriteTimeUtc
                };
            try
            {
                if (AddIISApp(newApp))
                {
                    this.installedApplications.Add(newApp);

                    tenantDeployment.Status = TenantDeploymentStatus.AppDeployed;
                    tenantDeployment.AppHostInstanceIp = Helper.HostIpAddress;
                    tenantDeployment.AppHostInstanceId = RoleEnvironment.CurrentRoleInstance.Id;
                    TenantDeploymentsStore.UpdateDeploymentStore(tenantDeployment);

                    return tenantDeployment;
                }
            }
            catch (Exception exception)
            {
                // remove this entry from both in-memory appList and tenant table
                if (this.installedApplications.Any(app => app.ApplicationHostName.Equals(newApp.ApplicationHostName)))
                {
                    this.installedApplications.Remove(newApp);
                }

                if (TenantDeploymentsStore.Instance.TenantDeployment.Any(td => td.HostName.Equals(newApp.ApplicationHostName)))
                {
                    TenantDeploymentsStore.Instance.DeleteObject(tenantDeployment);
                    TenantDeploymentsStore.Instance.SaveChanges();
                }

                Trace.TraceError(
                    "Error deploying the application {0} for tenant {1} on server {2}. Details are: {3}",
                    tenantDeployment.Application,
                    tenantDeployment.TenantId,
                    RoleEnvironment.CurrentRoleInstance.Id,
                    exception);
            }

            tenantDeployment.Status = TenantDeploymentStatus.DeploymentFailed;
            return tenantDeployment;
        }
    }
}
